/* Copyright (c) 2011-2012 ISP RAS, Moscow. For more information please write to
 
      mermakov@ispras.ru
      svartanov@ispras.ru

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 */

package ru.ispras.coffeemachine.instrument.util;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.bcel.Repository;
import org.apache.bcel.classfile.Constant;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.BranchInstruction;
import org.apache.bcel.generic.ClassGen;
import org.apache.bcel.generic.CodeExceptionGen;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.InstructionTargeter;

public class Utility {

	private static final String COFFEEMACHINE_STR = "coffeemachine";
	private static final String CLINIT_METHOD = "<clinit>";

	/**
	 * Checks parent-child relation between two classes
	 * 
	 * @param targetClass target class
	 * @param baseClass parent class
	 * @return <code>true</code> if target class extends parent class, <code>false</code> otherwise
	 */
	public static boolean extendsSuperClass(JavaClass targetClass, JavaClass baseClass) {

		try {
			return (Arrays.binarySearch(targetClass.getSuperClasses(), baseClass) >= 0);
		} catch (ClassNotFoundException e) {
			return false;
		} catch (Exception e) {
			return false;
		}
	}

	/**
	 * Checks parent-child relation between two classes
	 * 
	 * @param targetClass target class
	 * @param baseClass parent class name
	 * @return <code>true</code> if target class extends parent class, <code>false</code> otherwise
	 */
	public static boolean extendsSuperClass(JavaClass targetClass, String baseClass) {

		return extendsSuperClass(targetClass, Utility.lookupClass(baseClass));
	}

	/**
	 * Checks whether target class implements specified interface
	 * 
	 * @param targetClass target class
	 * @param name fully specified interface name (e.g. java.util.List)
	 * @return <code>true</code> if target class implements specified interface, <code>false</code>
	 *         otherwise
	 */
	public static boolean implementsInterface(JavaClass targetClass, String name) {

		String[] interfaceNames = targetClass.getInterfaceNames();

		for (String interfaceName : interfaceNames) {
			if (interfaceName.equals(name)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Returns {@link JavaClass} with specified name
	 * 
	 * @param name class name
	 * @return {@link JavaClass} with specified name if classpath contains one, <code>null</code>
	 *         otherwise
	 */
	public static JavaClass lookupClass(String name) {

		try {
			return Repository.lookupClass(name);
		} catch (ClassNotFoundException e) {
			return null;
		}
	}

	/**
	 * Checks whether target class contains a method with specified name and signature
	 * 
	 * @param targetClass target class
	 * @param methodName method name
	 * @param methodSignature fully specified method signature (e.g.
	 *        {@link java.lang.String#charAt(int) charAt(I)C})
	 * @return <code>true</code> if target class contains method with specified parameters,
	 *         <code>false</code> otherwise
	 */
	public static boolean containsMethod(JavaClass targetClass, String methodName,
											String methodSignature) {

		if (targetClass == null) {
			return false;
		}

		Method[] methods = targetClass.getMethods();

		for (Method method : methods) {
			if ((method.getName() + method.getSignature()).equals(methodName + methodSignature)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Returns target class field with specified name
	 * 
	 * @param targetClass target class
	 * @param name field name
	 * @return requested field reference or <code>null</code> if target class doesn't include a
	 *         field with specified name
	 */
	public static Field findVariable(JavaClass targetClass, String name) {

		if ((targetClass == null) || (name == null)) {
			return null;
		}

		int dotIndex = name.lastIndexOf('.');

		if (dotIndex != -1) {
			String exactName = name.substring(dotIndex + 1);
			do {
				for (Field field : targetClass.getFields()) {
					if (field.getName().equals(exactName)) {
						return field;
					}
				}
				try {
					targetClass = targetClass.getSuperClass();
				} catch (ClassNotFoundException e) {
					break;
				}
			} while (targetClass != null);
		}

		return null;
	}

	/**
	 * Checks whether a field with specified name in target class is final
	 * 
	 * @param targetClass target class
	 * @param name field name
	 * @return <code>true</code> if requested field is final, <code>false</code> otherwise
	 */
	public static boolean isFinalVariable(JavaClass targetClass, String name) {

		Field field = findVariable(targetClass, name);
		if (field != null) {
			return field.isFinal();
		}

		return false;
	}

	/**
	 * Checks whether a field with specified name in target class is volatile
	 * 
	 * @param targetClass target class
	 * @param name field name
	 * @return <code>true</code> if requested field is volatile, <code>false</code> otherwise
	 */
	public static boolean isVolatileVariable(JavaClass targetClass, String name) {

		Field field = findVariable(targetClass, name);
		if (field != null) {
			return field.isVolatile();
		}

		return false;
	}

	/**
	 * Checks whether target class has already been instrumented and contains Daemon/Concurrency
	 * invokes.
	 * 
	 * @param targetClass target class
	 * @return <code>true</code> if target class has already been instrumented, <code>false</code>
	 *         otherwise
	 */
	public static boolean isInstrumented(JavaClass targetClass) {

		for (Constant constant : targetClass.getConstantPool().getConstantPool()) {
			if (constant != null) {
				if (constant.toString().contains(COFFEEMACHINE_STR)) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Checks whether target class contains <code>\<clinit\></code> method.
	 * 
	 * @param targetClass target class
	 * @return <code>true</code> if target class contains <code>\<clinit\></code> method,
	 *         <code>false</code> otherwise
	 */
	public static boolean containsClinitMethod(JavaClass targetClass) {

		for (Method method : targetClass.getMethods()) {
			if (method != null) {
				if (method.getName() != null) {
					if (method.getName().contains(CLINIT_METHOD)) {
						return true;
					}
				}
			}
		}

		return false;
	}

	/**
	 * Returns only {@link CodeExceptionGen} targeters of instruction handle
	 * 
	 * @param ih target instruction handle
	 * @return a list of instruction handle exception targeters
	 */
	public static List<CodeExceptionGen> getExceptionTargeters(InstructionHandle ih) {

		List<CodeExceptionGen> result = new ArrayList<CodeExceptionGen>();
		if (!ih.hasTargeters()) {
			return result;
		}
		InstructionTargeter[] targeters = ih.getTargeters();
		for (InstructionTargeter targeter : targeters) {
			if (targeter instanceof CodeExceptionGen) {
				result.add((CodeExceptionGen)targeter);
			}
		}

		return result;
	}

	public static boolean hasExceptionTargeters(InstructionHandle ih) {

		if (!ih.hasTargeters()) {
			return false;
		}

		for (InstructionTargeter targeter : ih.getTargeters()) {
			if (targeter instanceof CodeExceptionGen) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Returns only {@link BranchInstruction} targeters of instruction handle, if it has any
	 * 
	 * @param ih target instruction handle
	 * @return jump targeters of instruction handle, <code>null</code> if it doesn't have any
	 */
	public static List<BranchInstruction> getJumpTargeters(InstructionHandle ih) {

		List<BranchInstruction> result = new ArrayList<BranchInstruction>();
		if (!ih.hasTargeters()) {
			return result;
		}
		InstructionTargeter[] targeters = ih.getTargeters();
		for (InstructionTargeter targeter : targeters) {
			if (targeter instanceof BranchInstruction) {
				result.add((BranchInstruction)targeter);
			}
		}

		return result;
	}

	public static void redirectExceptionHandler(CodeExceptionGen handler, InstructionHandle target,
												InstructionHandle redirect) {

		if (handler == null) {
			return;
		}

		if (handler.getStartPC() == target) {
			handler.setStartPC(redirect);
		}
		if (handler.getEndPC() == target) {
			handler.setEndPC(redirect);
		}
		if (handler.getHandlerPC() == target) {
			handler.setHandlerPC(redirect);
		}
	}

	public static int getCodeSize(JavaClass target) {

		int result = 0;
		for (Method method : target.getMethods()) {
			if (method.getCode() != null) {
				result += method.getCode().getLength();
			}
		}

		return result;
	}

	public static int getCodeSize(ClassGen target) {

		int result = 0;
		for (Method method : target.getMethods()) {
			if (method.getCode() != null) {
				result += method.getCode().getLength();
			}
		}

		return result;
	}
	
	public static boolean isExceptionHandler(List<CodeExceptionGen> exceptionHandlers, InstructionHandle ih) {
		
		for (CodeExceptionGen exceptionHandler : exceptionHandlers) {
			if (exceptionHandler.getHandlerPC() == ih) {
				return true;
			}
		}
		
		return false;
	}

}
